unit UTable;

interface

uses
  Types, SysUtils, Classes, IniFiles, Generics.Collections,
  UColumns, UPosition, UField;

const
{$INCLUDE UTable.inc}

type
  TTableAction = (taView, taInsert, taModify, taDelete);

  TTableActions = set of TTableAction;
const
  AllActions = [taView..taDelete];

type
  TTable = class(TObjectList<TSchField>)
  protected
    FRefTables: TRefTableDefObjectList;
    FPrimaryKey: TSchField;
    FDeinit: boolean;
    FPrimaryKeyName: string;
    FActions: TTableActions;
    FRefFields: TSchLookupFields;
    function GetSelectStr: string;
    function GetCaption(AName: string): string;
    function GetFromStr: string;
    procedure SetUpdateStrings;
    procedure Notify(const Item: TSchField; Action: TCollectionNotification); override;
  public
    UID, Name, Caption: string;
    Order: integer;
    Position: TFormPosition;
    Columns: TColumns;
    Primary: Boolean;
    SQL: TSQLRec;
    function GetTableDef(Name: string): TRefTableDef;
    function FieldByAlias(const X: string): TSchField;
    function FieldByRefKey(const X: string): TSchLookupField;
    function GetEQTableDef(const Key, RefTable, RefKey: string): TRefTableDef;
    function Fmt(const s: string): string;
    function AddField(f: TIniFile; FieldName: string): TSchField;
    property Actions: TTableActions read FActions;
    property PrimaryKey: TSchField read FPrimaryKey;
    property FieldCaptions[AName: string]: string read GetCaption;
    property RefFields: TSchLookupFields read FRefFields;
    procedure SaveToFile(f: TIniFile);
    procedure SaveAndDeinit(f: TIniFile);
    procedure Deinit;
    constructor Create; overload;
    constructor Create(f: TIniFile; const AUID: string); overload;
  end;

  TTables = TList<TTable>;

var
  TableNumber: integer;
  SchedTable: TTable;
  Tables: array of TTable;
  PrimaryTables: TTables;
  ConfigFile: TIniFile;
  InitFile: TIniFile;
  SchedName: string;

function WorkingDir: string;
function CreateTable(Name, Caption: string; f: TIniFile; ord: integer): TTable;

implementation

var
  ConfigPath: string = 'config\config.ini';
  InitPath: string = 'config\tables.ini';

function WorkingDir: string;
begin
  //it may be, for example, ExtractFilePath(paramstr(0)), but not now
  //because of changing the executable directory
  Result := GetCurrentDir + '\';
end;

function CreateTable(Name, Caption: string; f: TIniFile; ord: integer): TTable;
begin
  Result := TTable.Create(f, Name);
  Result.Caption := Caption;
  Result.Order := ord;
  if Result.Primary then
    PrimaryTables.Add(Result);
end;

{ TTable }

constructor TTable.Create(f: TIniFile; const AUID: string);
var
  Fields: THashedStringList;
  i: integer;
begin
  Create;
  Fields := THashedStringList.Create;
  UID := AUID;
  Caption := UID;
  Name := f.ReadString(Fmt(CTableConfig), 'table_name', UID);
  Position.LoadFromFile(f, Fmt(CWndPosition));
  Columns := TColumns.Create(UID, Fields, True).LoadFromFile(f);
  FPrimaryKeyName :=
    f.ReadString(Fmt(CTableConfig), 'primary_key_name', PrimaryKey_Name);
  f.ReadSectionValues(Fmt(CColumns), Fields);
  for i := 0 to Fields.Count - 1 do
    AddField(f, Fields.Names[i]);
  if FPrimaryKey = nil then
    Exclude(FActions, taInsert);
  Primary := f.ReadBool(Fmt(CTableConfig), 'primary', False);
end;

function TTable.AddField(f: TIniFile; FieldName: string): TSchField;
var
  IsRef: boolean;
  fld: TSchField;
  ref: TSchLookupField absolute fld;
function ReadFrom(const Sec: string; const Def: string = ''): string;
begin
  Result := f.ReadString(Fmt(Sec), FieldName, Def);
  if Result = '' then
    raise Exception.CreateFmt(CInvalidSection, [Fmt(Sec), f.FileName]);
end;
begin
  IsRef := f.ValueExists(Fmt(CRefTables), FieldName);
  fld := GetFieldClass(IsRef).Create(FieldName);
  fld.Caption := ReadFrom(CColumns, fld.Name);
  fld.Short := ReadFrom(CShortNames, fld.Caption);
  if IsRef then begin
    FRefFields.Add(ref);
    ref.Name := ReadFrom(CRefResults);
    ref.TableDef := GetEQTableDef(
      ReadFrom(CRefKeys), ReadFrom(CRefTables),
      ReadFrom(CRefFields, PrimaryKey_Name)
    );
    ref.SortField := ReadFrom(CPrefSort, ref.Name);
    ref.Order := ref.TableDef.Index;
  end else if SameText(fld.Name, FPrimaryKeyName) then
    FPrimaryKey := fld;
  Result := Items[Add(fld)];
end;

function TTable.GetFromStr: string;
var
  t: TRefTableDef;
begin
  Result := Format('FROM %s a0', [Name]);
  if FRefTables = nil then
    Exit;
  for t in FRefTables do
    Result := Result + t.GetJoin;
end;

function TTable.GetSelectStr: string;
var
  i: integer;
  t: TRefTableDef;
begin
  if Count = 0 then
    Exit('');
  Result := CSelect + Self[0].GetQueryField;
  for i := 1 to Count -1 do
    Result := Result + CComma + Self[i].GetQueryField;
  for t in FRefTables do begin
    if FieldByAlias(t.Key) <> nil then
      Continue;
    Result := Result + Format(', a0.%s as "%0:s"', [t.Key]);
  end;
end;

function TTable.GetTableDef(Name: string): TRefTableDef;
begin
  for Result in FRefTables do
    if Result.RefTable = Name then
      Exit;
  Result := nil;
end;

function TTable.GetEQTableDef(const Key, RefTable, RefKey: string): TRefTableDef;
var
  t, tt: TRefTableDef;
begin
  t := TRefTableDef.Create(Key, RefTable, RefKey);
  for tt in FRefTables do
    if TableDefsEqual(t, tt) then begin
      FreeAndNil(t);
      Exit(tt);
    end;
  t.Index := FRefTables.Add(t) + 1;
  Result := t;
end;

procedure TTable.Notify(const Item: TSchField; Action: TCollectionNotification);
begin
  inherited;
  if FDeinit then
    Exit;
  if (Action = cnAdded) and not Item.OK then
    raise Exception.Create('New field item was not fully filled');
  SQL.SelectStr := GetSelectStr;
  SQL.FromStr := GetFromStr;
  SetUpdateStrings;
end;

procedure TTable.SetUpdateStrings;
const
  CLineBreaks: array[0..3] of string = (CComma, CComma, CComma, CAnd);
var
  s, ss: string;
  Ins, Mdf, Fields, Where: TStringList;
  a: TPObjectDynArray;
  i: integer;
  t: TRefTableDef;
  f: TSchField;
function FmtWh(const s: string): string; inline;
begin
  Result := Format(CWhereCond, [s]);
end;
function ConjLB(S: TStrings): string;
begin
  Result := Conjuct(S, S.LineBreak);
end;
begin
  a := TPObjectDynArray.Create(@Ins, @Mdf, @Fields, @Where);
  for i := 0 to Length(a) - 1 do begin
    a[i]^ := TStringList.Create;
    TStringList(a[i]^).LineBreak := CLineBreaks[i];
  end;
  if FPrimaryKey <> nil then
    Where.Append(FmtWh(FPrimaryKey.Name));
  for t in FRefTables do begin
    Where.Append(FmtWh(t.Key));
    Fields.Append(t.Key);
  end;
  for f in Self do
    if not (f is TSchLookupField) and not (f = FPrimaryKey) then
      Fields.Append(f.Name);
  for s in Fields do  begin
    ss := CDoubleDot + s;
    Ins.Append(ss);
    Mdf.Append(s + CEqualis + ss);
  end;
  SQL.InsertStr := Format(Insertrequest, [Name, ConjLB(Fields), ConjLB(Ins)]);
  SQL.ModifyStr := Format(ModifyRequest, [Name, ConjLB(Mdf), ConjLB(Where)]);
  SQL.DeleteStr := Format(DeleteRequest, [Name, ConjLB(Where)]);
  for i := 0 to Length(a) - 1 do
    FreeAndNil(a[i]^);
end;


function TTable.FieldByAlias(const X: string): TSchField;
begin
  for Result in Self do
    if SameText(Result.Alias, X) then
      Exit;
  Result := nil;
end;


function TTable.FieldByRefKey(const X: string): TSchLookupField;
var
  f: TSchField;
  r: TSchLookupField absolute f;
begin
  for f in Self do
    if (f is TSchLookupField) and (r.TableDef.Key = X) then
      Exit(r);
  Result := nil;
end;

procedure TTable.Deinit;
begin
  FDeinit := True;
  FreeAndNil(FRefFields);
  FreeAndNil(Columns);
  FreeAndNil(FRefTables);
end;

procedure TTable.SaveAndDeinit(f: TIniFile);
begin
  SaveToFile(f);
  Deinit;
end;

function TTable.Fmt(const s: string): string;
begin
  Result := Format(s, [UID]);
end;

function TTable.GetCaption(AName: string): string;
begin
  Result := FieldByAlias(AName).Caption;
end;

procedure TTable.SaveToFile(f: TIniFile);
begin
  Position.SaveToFile(f, Format(CWndPosition, [UID]));
  Columns.SaveToFile(f);
end;

constructor TTable.Create;
begin
  inherited;
  FRefTables := TRefTableDefObjectList.Create;
  FRefFields := TSchLookupFields.Create;
  FActions := AllActions;
end;


procedure InitializeUnit;
var
  i, scOrd: integer;
  TablesList: THashedStringList;
function InternalCreate(Name, Caption: string; f: TIniFile; ord: integer): TTable;
begin
  Result := CreateTable(Name, Caption, f, ord);
  if SameText(Result.Name, SchedName) then
    scOrd := i;
end;
begin
  ConfigPath := WorkingDir + ConfigPath;
  InitPath := WorkingDir + InitPath;
  InitFile := TIniFile.Create(InitPath);
  ConfigFile := TIniFile.Create(ConfigPath);
  TablesList:= THashedStringList.Create;
  PrimaryTables := TTables.Create;
  SchedName := InitFile.ReadString(CHexeract, 'sched_table', 'schedule');
  InitFile.ReadSectionValues(TablesSection, TablesList);
  TableNumber := TablesList.Count;
  SetLength(Tables, TableNumber);
  for i := 0 to TableNumber - 1 do
    with TablesList do
      Tables[i] := InternalCreate(Names[i], ValueFromIndex[i], ConfigFile, i);
  SchedTable :=
    CreateTable(SchedName, TablesList.Values[SchedName], ConfigFile, scOrd);
  FreeAndNil(TablesList);
end;

procedure FinalizeUnit;
var
  i: integer;
begin
  FreeAndNil(PrimaryTables);
  for i := 0 to TableNumber - 1 do begin
    Tables[i].SaveAndDeinit(ConfigFile);
    FreeAndNil(Tables[i]);
  end;
  SchedTable.Deinit;
  FreeAndNil(SchedTable);
  FreeAndNil(ConfigFile);
  FreeAndNil(InitFile);
end;

initialization
  InitializeUnit;

finalization
  FinalizeUnit;

end.
